package ro.sergiu.configmanager;

import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import ro.sergiu.configmanager.annotations.ConfigSource;
import ro.sergiu.configmanager.annotations.ConfigSourceType;
import ro.sergiu.configmanager.annotations.ErrorBehaviorType;
import ro.sergiu.configmanager.annotations.Ignore;
import ro.sergiu.configmanager.annotations.PropertyName;

/**
 *
 * @author Toporjinschi
 */
public final class ConfigContainer {

    private ConfigContainer() {
    }

    private static final String GET = "get";
    private static final String IS = "is";
    private static final String HAS = "has";
    private static final String SET = "set";
    private static final String PLACEHOLDER_REGEX = "\\$\\{(.*?)\\}";
    private static final Map<Class, Object> CONFIGS = new HashMap<Class, Object>();

    /**
     * Loads, in container, the settings for one specific bean
     *
     * @param clazz the loading bean class
     * @throws InstantiationException if the creation of instance is failing
     * @throws IllegalAccessException if the there is a set method inaccessible
     * @throws IOException if the {@link ErrorBehaviorType} is set on THROW_ERROR and file cannot be
     * read;
     */
    public static void loadConfigurations(Class<?> clazz) throws InstantiationException, IllegalAccessException, IOException {
        ConfigSource source = clazz.getAnnotation(ConfigSource.class);
        if (source.type() == ConfigSourceType.FILE) {
            File file = getCofigFile(source.value());
            Object instance = null;
            if (!file.exists() || !file.canRead()) {
                if (source.errorBehavior() == ErrorBehaviorType.THROW_ERROR) {
                    throw new RuntimeException("Could not read the file " + source.value());
                } else if (source.errorBehavior() == ErrorBehaviorType.LOAD_DEFAULT) {
                    instance = clazz.newInstance();
                }
            } else {
                instance = injectValues(clazz, file);
            }
            if (instance == null) {
                throw new NullPointerException("Instance should not be null");
            }
            CONFIGS.put(clazz, instance);
        }
    }

    private static File getCofigFile(String filePath) {
        Pattern p = Pattern.compile(PLACEHOLDER_REGEX);
        Matcher m = p.matcher(filePath);
        while (m.find()) {
            String placeHolder = m.group();
            String systemVar = placeHolder.substring(2, placeHolder.indexOf("}"));
            String value = System.getProperty(systemVar) == null ? "" : System.getProperty(systemVar);
            filePath = filePath.replace(placeHolder, value);
        }
        return new File(filePath);
    }

    private static Object injectValues(Class clazz, File file) throws InstantiationException, IllegalAccessException, IOException {
        Object config = clazz.newInstance();
        Properties prop = new Properties();
        prop.load(new FileReader(file));
        Field[] declaredFields = clazz.getDeclaredFields();
        for (Field field : declaredFields) {
            if (field.isAnnotationPresent(Ignore.class)) {
                continue;
            }
            String fieldName = field.getName();
            String value;
            if (field.isAnnotationPresent(PropertyName.class)) {
                value = prop.getProperty(field.getAnnotation(PropertyName.class).value());
            } else {
                value = prop.getProperty(fieldName);
            }
            if (value == null) {
                continue;
            }
            Class<?> type = field.getType();
            injectFieldValue(fieldName, value, type, clazz, config);
        }
        return config;
    }

    private static void injectFieldValue(String name, String value, Class<?> type, Class<?> clazz, Object o) {
        String methodName = SET + name.substring(0, 1).toUpperCase() + name.substring(1);
        try {
            Method method = clazz.getMethod(methodName, type);
            if (type == String.class) {
                method.invoke(o, value);
            } else if (type == Integer.class) {
                method.invoke(o, Integer.parseInt(value));
            } else if (type == Boolean.class) {
                method.invoke(o, Boolean.parseBoolean(value));
            } else if (type == Float.class) {
                method.invoke(o, Float.parseFloat(value));
            } else if (type == Double.class) {
                method.invoke(o, Double.parseDouble(value));
            } else if (type == Long.class) {
                method.invoke(o, Long.parseLong(value));
            } else if (type == Byte.class) {
                method.invoke(o, Byte.parseByte(value));
            }
        } catch (NoSuchMethodException ex) {
            Logger.getLogger(ConfigContainer.class.getName()).log(Level.WARNING, "Property " + name + " ignored", ex);
        } catch (SecurityException ex) {
            Logger.getLogger(ConfigContainer.class.getName()).log(Level.WARNING, "Property " + name + " ignored", ex);
        } catch (IllegalAccessException ex) {
            Logger.getLogger(ConfigContainer.class.getName()).log(Level.WARNING, "Property " + name + " ignored", ex);
        } catch (IllegalArgumentException ex) {
            Logger.getLogger(ConfigContainer.class.getName()).log(Level.WARNING, "Property " + name + " ignored", ex);
        } catch (InvocationTargetException ex) {
            Logger.getLogger(ConfigContainer.class.getName()).log(Level.WARNING, "Property " + name + " ignored", ex);
        }
    }

    /**
     * Returns the instance of a loaded bean; This method will return null if the instance was not
     * already loaded;
     *
     * @param <T> the class type of bean
     * @param clazz bean class which should be returned
     * @return an instance, populated with the configurations
     */
    public static <T> T getConfig(Class<T> clazz) {
        return (T) CONFIGS.get(clazz);
    }

    /**
     * Returns the instance of a loaded bean; This method will try to load the configurations before
     * returning the instance, if is not possible then errors will be logged and null will be
     * returned;
     *
     * @param <T> the class type of bean
     * @param clazz bean class which should be returned
     * @param loadIt set as true to load the instance if is not loaded; if the loading fails returns
     * null;
     * @return an instance, populated with the configurations
     */
    public static <T> T getConfig(Class<T> clazz, boolean loadIt) {
        T instance = (T) CONFIGS.get(clazz);
        if (loadIt && instance == null) {
            try {
                loadConfigurations(clazz);
            } catch (InstantiationException ex) {
                Logger.getLogger(ConfigContainer.class.getName()).log(Level.WARNING, "Could not load settings for " + clazz.getName(), ex);
            } catch (IllegalAccessException ex) {
                Logger.getLogger(ConfigContainer.class.getName()).log(Level.WARNING, "Could not load settings for " + clazz.getName(), ex);
            } catch (IOException ex) {
                Logger.getLogger(ConfigContainer.class.getName()).log(Level.WARNING, "Could not load settings for " + clazz.getName(), ex);
            }
        }
        return (T) CONFIGS.get(clazz);
    }

    /**
     * Saves a configuration bean;
     *
     * @param <T> the type of configuration bean
     * @param clazz the class of the bean which needs to be saved;
     * @throws IOException in case of an error when saving in a file;
     */
    public static <T> void saveConfig(Class<T> clazz) throws IOException {
        ConfigSource source = clazz.getAnnotation(ConfigSource.class);
        if (source.type() == ConfigSourceType.FILE) {
            File file = getCofigFile(source.value());
            if (file.exists() && !file.canWrite()) {
                throw new RuntimeException("Could not write the file " + source.value());
            }
            T obj = getConfig(clazz);
            Properties prop = createProperties(clazz, obj);
            FileWriter fileWriter = new FileWriter(file);
            prop.store(fileWriter, "");
            fileWriter.close();
        }
    }

    private static <T> Properties createProperties(Class<T> clazz, Object obj) {
        Properties prop = new Properties();
        Field[] declaredFields = clazz.getDeclaredFields();
        for (Field field : declaredFields) {
            if (field.isAnnotationPresent(Ignore.class)) {
                continue;
            }
            String fieldName = field.getName();
            Class<?> type = field.getType();
            String value = getFieldValue(fieldName, type, clazz, obj);
            if (value == null) {
                continue;
            }
            if (field.isAnnotationPresent(PropertyName.class)) {
                prop.setProperty(field.getAnnotation(PropertyName.class).value(), value);
            } else {
                prop.setProperty(fieldName, value);
            }
        }
        return prop;
    }

    private static String getFieldValue(String fieldName, Class<?> type, Class<?> clazz, Object obj) {
        String[] possibleMethodNames = new String[]{
            GET + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1),
            IS + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1),
            HAS + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1)
        };
        Method method = null;
        for (int i = 0; i < possibleMethodNames.length; i++) {
            try {
                System.out.println(possibleMethodNames[i]);
                method = clazz.getMethod(possibleMethodNames[i]);
                break;
            } catch (NoSuchMethodException ex) {
                if (possibleMethodNames.length - 1 == i) {
                    Logger.getLogger(ConfigContainer.class.getName()).log(Level.WARNING, "Property " + fieldName + " ignored", ex);
                    return null;
                }
            }
        }
        if (method != null && method.getReturnType() != type) {
            Logger.getLogger(ConfigContainer.class.getName()).log(Level.WARNING, "Property " + fieldName + " ignored", new Exception("The method " + method.getName() + " returns a different type."));
            return null;
        }

        try {
            return invokeMethod(method, obj);
        } catch (IllegalAccessException ex) {
            Logger.getLogger(ConfigContainer.class.getName()).log(Level.WARNING, "Property " + fieldName + " ignored", ex);
        } catch (IllegalArgumentException ex) {
            Logger.getLogger(ConfigContainer.class.getName()).log(Level.WARNING, "Property " + fieldName + " ignored", ex);
        } catch (InvocationTargetException ex) {
            Logger.getLogger(ConfigContainer.class.getName()).log(Level.WARNING, "Property " + fieldName + " ignored", ex);
        }
        return null;
    }

    private static String invokeMethod(Method method, Object obj) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
        Object value = method.invoke(obj);
        if (value == null) {
            return null;
        } else if (method.getReturnType() == Boolean.class) {
            return Boolean.toString((Boolean) value);
        } else if (method.getReturnType() == Integer.class) {
            return Integer.toString((Integer) value);
        } else if (method.getReturnType() == Float.class) {
            return Float.toString((Float) value);
        } else if (method.getReturnType() == Double.class) {
            return Double.toString((Double) value);
        } else if (method.getReturnType() == Long.class) {
            return Long.toString((Long) value);
        } else if (method.getReturnType() == Byte.class) {
            return Byte.toString((Byte) value);
        } else {
            return (String) value;
        }
    }
}
